﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace AMOOF {

    /*
     *	An immutable collection of buckets.  This class is used by the Histogram
     *	class defined below to represent the bars in the histogram graph bitmap.
     *	This class only maintains the count associated with each bucket; all
     *	color and bitmap information is handled by the Histogram class itself.
     */
    public class BucketList {
        private double _leftEdge;
        private double _rightEdge;
        private int _maxBucket;
        private long _bucketTotal;
        private int[] _buckets;

        public double LeftEdge { get { return _leftEdge; } }
        public double RightEdge { get { return _rightEdge; } }
        public int LargestBucket { get { return _maxBucket; } }
        public int[] Buckets { get { return _buckets; } }
        public int BucketCount { get { return _buckets.Length; } }
        public long SumTotal { get { return _bucketTotal; } }

        public BucketList() {
            _leftEdge = 0.0;
            _rightEdge = 1.0;
            _maxBucket = 0;
        }

        public BucketList(SortedDictionary<double, int> dataset, int bucketCount) {
            this.ImportDataset(dataset, bucketCount, dataset.First().Key, dataset.Last().Key);
        }

        public BucketList(SortedDictionary<double, int> dataset, int bucketCount, double start, double end) {
            this.ImportDataset(dataset, bucketCount, start, end);
        }

        public BucketList(SortedDictionary<double, int> dataset, BucketsDescriptor bucketStats) {
            this.ImportDataset(dataset, bucketStats.BucketCount, bucketStats.Start, bucketStats.End);
        }

        // collect a dataset into buckets
        public static double barWidth2 = 0;
        private void ImportDataset(SortedDictionary<double, int> dataset, int bucketCount, double start, double end) {
            _maxBucket = 0;
            _bucketTotal = 0;
            _leftEdge = start;
            _rightEdge = end;
            _buckets = new int[bucketCount];

            int currentBar = 0;
            double barWidth = (_rightEdge - _leftEdge) / bucketCount;
            barWidth2 = barWidth;
            double bucketLimit = _leftEdge + barWidth;
            foreach (KeyValuePair<double, int> sample in dataset) {
                // skip samples out of range
                if (sample.Key < _leftEdge)
                    continue;

                if (sample.Key > bucketLimit) {   // we're done with this bucket

                    // skip samples out of range
                    if (sample.Key >= _rightEdge)
                        return;

                    // start a new bucket
                    currentBar = (int)Math.Floor((sample.Key - _leftEdge) / barWidth);
                    bucketLimit = _leftEdge + (currentBar + 1) * barWidth;
                }

                // add the sample to the current bucket
                _buckets[currentBar] += sample.Value;

                // check for new maximum bucket
                if (_buckets[currentBar] > _maxBucket)
                    _maxBucket = _buckets[currentBar];

                // total up buckets
                // WARN: global max, or ranged max??
                _bucketTotal += sample.Value;
            }
        }

        // (implicitly) converts a BucketList to a BucketsDescriptor
        public static implicit operator BucketsDescriptor(BucketList buckets) {
            return new BucketsDescriptor(buckets.BucketCount, buckets.LeftEdge, buckets.RightEdge);
        }
    }

    /*
     *	a simple class representing histogram bitmap information
     */
    public class HistogramDescriptor {
        public Size ImageSize;
        public Color OutlineColor;
        public Color FillGradientStart;
        public Color FillGradientEnd;
        public int LineWeight;
        public bool Dirty;

        public int Width { get { return ImageSize.Width; } set { ImageSize.Width = value; } }
        public int Height { get { return ImageSize.Height; } set { ImageSize.Height = value; } }

        public HistogramDescriptor(int width, int height, Color fillStart, Color fillEnd, Color outlineColor, int lineWeight) {
            this.ImageSize = new Size(width, height);
            this.OutlineColor = outlineColor;
            this.FillGradientStart = fillStart;
            this.FillGradientEnd = fillEnd;
            this.LineWeight = lineWeight;
            this.Dirty = false;
        }

        public HistogramDescriptor Clone() {
            this.Dirty = false;

            return new HistogramDescriptor(this.ImageSize.Width, this.ImageSize.Height, this.FillGradientStart, this.FillGradientEnd, this.OutlineColor, this.LineWeight);
        }

        public bool Stale(HistogramDescriptor that) {
            return this.Dirty
                || that == null	// WARN: possibly doesn't catch the null
                || this.LineWeight != that.LineWeight
                || this.ImageSize != that.ImageSize
                || this.FillGradientStart != that.FillGradientStart
                || this.FillGradientEnd != that.FillGradientEnd
                || this.OutlineColor != that.OutlineColor;
        }
    }

    /*
     *	a simple class representing histogram data information
     */
    public class BucketsDescriptor {
        public int BucketCount;
        public double Start;
        public double End;

        public BucketsDescriptor(int bucketCount, double start, double end) {
            this.BucketCount = bucketCount;
            this.Start = start;
            this.End = end;
        }

        // compare two BucketsDescriptors for value equality
        public override bool Equals(object obj) {
            if (obj == null || this.GetType() != obj.GetType()) return false;
            BucketsDescriptor that = (BucketsDescriptor)obj;

            return this.BucketCount == that.BucketCount
                && this.Start == that.Start
                && this.End == that.End;
        }
    }

    /*
     *	This Histogram class represents a histogram bitmap.  The two most
     *	expensive operations, filling buckets and drawing the image, are only
     *	performed as needed.  This class supports multiple PDF overlays while
     *	maintaining compatibility with the previous version.
     */
    public class Histogram {

        public enum NegativeHandle {
            TRUNCATE,
            SHIFT
        }

        private SortedDictionary<double, int> _dataset;
        private BucketList _buckets;
        private Bitmap _image;
        private Dictionary<PDF, Color> _pdfs;

        const int DEFAULT_BAR_COUNT = 40;
        const int DEFAULT_LINE_WEIGHT = 2;
        const int HORI_TICKS = 5;
        const int VERT_TICKS = 5;
        const int TICK_LENGTH = 7;
        const float MINOR_TICK_SCALE = 0.4f;
        const double VERT_RES = 0.0625;
        const int VERT_LABEL_WIDTH = 40;
        const string HORI_LABEL_FORMAT = "G";
        const string VERT_LABEL_FORMAT = "0.0###";

        readonly static double DEFAULT_RANGE_START = 0.0;
        readonly static double DEFAULT_RANGE_END = 1.0;
        readonly static Color DEFAULT_GRADIENT_START = Color.Blue;
        readonly static Color DEFAULT_GRADIENT_END = Color.LightBlue;

        readonly static int MARGIN_PIXELS = 10;
        readonly static Color GRAPH_BACKGROUND_COLOR = Color.White;
        readonly static Color GRAPH_AXIS_COLOR = Color.Black;
        readonly static Color DEFAULT_LINE_COLOR = Color.Black;
        readonly static Color DEFAULT_PDF_COLOR = Color.Red;
        readonly static byte LEGEND_OPACITY = 0x8f;
        readonly static int DEFAULT_WIDTH = 256;
        readonly static int DEFAULT_HEIGHT = 128;

        readonly static float LABEL_FONT_SIZE = 7.5f;
        readonly static Font LABEL_FONT = new Font(FontFamily.GenericSansSerif, LABEL_FONT_SIZE);
        readonly static int DEFAULT_HORI_PRECISION = 5;

        // image properties
        private HistogramDescriptor _newImageStats;
        private HistogramDescriptor _oldImageStats;
        private BucketsDescriptor _bucketStats;

        public int BucketCount { get { return _bucketStats.BucketCount; } set { _bucketStats.BucketCount = value; } }
        public int BarCount { get { return _bucketStats.BucketCount; } set { _bucketStats.BucketCount = value; } }
        public double Start { get { return _bucketStats.Start; } set { _bucketStats.Start = value; } }
        public double End { get { return _bucketStats.End; } set { _bucketStats.End = value; } }
        public Size ImageSize { get { return _newImageStats.ImageSize; } set { _newImageStats.ImageSize = value; } }
        public int ImageWidth { get { return _newImageStats.Width; } set { _newImageStats.Width = value; } }
        public int ImageHeight { get { return _newImageStats.Height; } set { _newImageStats.Height = value; } }
        public Color FillGradientStart { get { return _newImageStats.FillGradientStart; } set { _newImageStats.FillGradientStart = value; } }
        public Color FillGradientEnd { get { return _newImageStats.FillGradientEnd; } set { _newImageStats.FillGradientEnd = value; } }
        public Color OutlineColor { get { return _newImageStats.OutlineColor; } set { _newImageStats.OutlineColor = value; } }
        public int LineWeight { get { return _newImageStats.LineWeight; } set { _newImageStats.LineWeight = value; } }

        // default constructor
        public Histogram() {
            _bucketStats = new BucketsDescriptor(DEFAULT_BAR_COUNT, DEFAULT_RANGE_START, DEFAULT_RANGE_END);
            _oldImageStats = null;
            _newImageStats = new HistogramDescriptor(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_GRADIENT_START, DEFAULT_GRADIENT_END, DEFAULT_LINE_COLOR, DEFAULT_LINE_WEIGHT);
            _pdfs = new Dictionary<PDF, Color>();
        }

        // simple constructor
        public Histogram(Dictionary<double, int> dataset) {
            _bucketStats = new BucketsDescriptor(DEFAULT_BAR_COUNT, DEFAULT_RANGE_START, DEFAULT_RANGE_END);
            _oldImageStats = null;
            _newImageStats = new HistogramDescriptor(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_GRADIENT_START, DEFAULT_GRADIENT_END, DEFAULT_LINE_COLOR, DEFAULT_LINE_WEIGHT);
            _pdfs = new Dictionary<PDF, Color>();

            this.ImportData(dataset);
        }

        public Histogram(List<Tuple<double, int>> dataset) {
            _bucketStats = new BucketsDescriptor(DEFAULT_BAR_COUNT, DEFAULT_RANGE_START, DEFAULT_RANGE_END);
            _oldImageStats = null;
            _newImageStats = new HistogramDescriptor(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_GRADIENT_START, DEFAULT_GRADIENT_END, DEFAULT_LINE_COLOR, DEFAULT_LINE_WEIGHT);
            _pdfs = new Dictionary<PDF, Color>();

            ImportData(dataset);
        }

        // quick constructor
        public Histogram(Dictionary<double, int> dataset, int barCount, Color fillColor) {
            _bucketStats = new BucketsDescriptor(barCount, DEFAULT_RANGE_START, DEFAULT_RANGE_END);
            _oldImageStats = null;
            _newImageStats = new HistogramDescriptor(DEFAULT_WIDTH, DEFAULT_HEIGHT, fillColor, fillColor, DEFAULT_LINE_COLOR, DEFAULT_LINE_WEIGHT);
            _pdfs = new Dictionary<PDF, Color>();

            this.ImportData(dataset);
        }

        // compatibility constructor
        public Histogram(Dictionary<double, int> dataset, bool sorted = false, int barCount = DEFAULT_BAR_COUNT, NegativeHandle handler = NegativeHandle.TRUNCATE) {
            _bucketStats = new BucketsDescriptor(barCount, DEFAULT_RANGE_START, DEFAULT_RANGE_END);
            _oldImageStats = null;
            _newImageStats = new HistogramDescriptor(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_GRADIENT_START, DEFAULT_GRADIENT_END, DEFAULT_LINE_COLOR, DEFAULT_LINE_WEIGHT);
            _pdfs = new Dictionary<PDF, Color>();

            this.ImportData(dataset);
            if (handler == NegativeHandle.TRUNCATE) {
                SortedDictionary<double, int>.Enumerator it = _dataset.GetEnumerator();
                it.MoveNext();
                while (it.Current.Key < 0.0)
                    it.MoveNext();
                _bucketStats.Start = it.Current.Key;
            }
        }

        // conversion constructor (from a List<Tuple> object)
        public Histogram(List<Tuple<double, int>> dataset, bool sorted = false, int barCount = DEFAULT_BAR_COUNT, NegativeHandle handler = NegativeHandle.TRUNCATE) {
            _bucketStats = new BucketsDescriptor(barCount, DEFAULT_RANGE_START, DEFAULT_RANGE_END);
            _oldImageStats = null;
            _newImageStats = new HistogramDescriptor(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_GRADIENT_START, DEFAULT_GRADIENT_END, DEFAULT_LINE_COLOR, DEFAULT_LINE_WEIGHT);
            _pdfs = new Dictionary<PDF, Color>();

            this.ImportData(dataset);
        }

        // import a list of data points into a (sorted) dataset
        public void ImportData(Dictionary<double, int> dataset, NegativeHandle handler = NegativeHandle.TRUNCATE) {
            _dataset = new SortedDictionary<double, int>(dataset);

            if (_dataset.Count > 0) {
                if (_dataset.First().Key < 0)
                    _bucketStats.Start = _dataset.First(sample => sample.Key >= 0).Key;
                else
                    _bucketStats.Start = _dataset.First().Key;


                _bucketStats.End = _dataset.Last().Key;
            }

            // importing a dataset always invalidates the image; this prevents
            // makes sure we redraw the image when the data set changes, even
            // if the BucketsDescriptor stays the same
            _newImageStats.Dirty = true;
        }

        // import a list of data points into a (sorted) dataset
        public void ImportData(List<Tuple<double, int>> dataset, NegativeHandle handler = NegativeHandle.TRUNCATE) {
            _dataset = new SortedDictionary<double, int>();

            // find minimum key
            //double shift;
            //if (handler == NegativeHandle.SHIFT)
            //{
            //    shift = dataset.Min().Item1; // WARN: THIS LINE IS WRONG
            //    if (shift > 0)
            //        shift = 0;
            //}
            //else
            //    shift = 0;

            // copy each element
            foreach (Tuple<double, int> b in dataset) {
                if (b.Item2 < 0)
                    throw new ArgumentOutOfRangeException("dataset", "Populations must be greater than or equal to zero.");

                if (_dataset.ContainsKey(b.Item1)) {
                    _dataset[b.Item1] += b.Item2;
                }
                else
                    _dataset.Add(b.Item1, b.Item2);
            }

            if (_dataset.Count > 0) { // truncate negative samples
                if (_dataset.First().Key < 0)
                    _bucketStats.Start = _dataset.First(sample => sample.Key >= 0).Key;
                else
                    _bucketStats.Start = _dataset.First().Key;

                _bucketStats.End = _dataset.Last().Key;
            }
            _newImageStats.Dirty = true;
        }

        // adds a single PDF to the histogram
        public void AddPDF(PDF newPDF, Color pdfColor) {
            // do we already have this PDF?
            if (_pdfs.ContainsKey(newPDF)) {
                // PDF already in list, compare colors
                if (_pdfs[newPDF] != pdfColor) {
                    // colors differ, update PDF
                    _pdfs[newPDF] = pdfColor;
                    _newImageStats.Dirty = true;
                }
            }
            else {
                // PDF not yet in list
                _pdfs[newPDF] = pdfColor;
                _newImageStats.Dirty = true;
            }
        }

        // removes all PDFs from the histogram
        public void ClearPDFs() {
            // we don't want to redraw if the PDF list is already empty
            if (_pdfs.Count != 0) {
                _pdfs.Clear();
                _newImageStats.Dirty = true;
            }
        }

        public Bitmap GetHistogram() {
            // check for stale bitmap
            if (!_bucketStats.Equals(_buckets)) {
                _buckets = new BucketList(_dataset, _bucketStats);
                this.RenderHistogram();
            }
            else if (_newImageStats.Stale(_oldImageStats))
                this.RenderHistogram();

            return _image;
        }

        /*
         *	Performs the actual drawing.
         */
        private void RenderHistogram() {
            // set up the drawing canvas
            _image = new Bitmap(_newImageStats.ImageSize.Width, _newImageStats.ImageSize.Height);
            Graphics g = Graphics.FromImage(_image);
            g.SmoothingMode = SmoothingMode.HighQuality;
            g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
            Pen linePen;
            LinearGradientBrush gradientBrush;
            g.Clear(GRAPH_BACKGROUND_COLOR);

            // WARN: too small of a bitmap would be bad
            int margin = MARGIN_PIXELS;
            // the margin is a percentage of the smaller dimension
            //int margin = (int)Math.Ceiling(Math.Min(_newImageStats.ImageSize.Height, _newImageStats.ImageSize.Width) * MARGIN_PERCENT);

            // determine maximum width of vertical axis labels
            //float labelWidth = stringWidth(new string('0', (int)Math.Ceiling(Math.Log10(_buckets.LargestBucket))), LABEL_FONT);
            float labelWidth = stringWidth("0.000", LABEL_FONT);

            // determine scale
            int graphBottom = _newImageStats.ImageSize.Height - TICK_LENGTH - LABEL_FONT.Height - margin;
            int graphHeight = graphBottom - (int)Math.Ceiling(0.5 * LABEL_FONT.Height) - margin;
            int graphLeft = margin + (int)labelWidth + TICK_LENGTH;
            int graphWidth = _newImageStats.ImageSize.Width - graphLeft - margin;

            double maxY = VERT_RES * Math.Ceiling(((double)_buckets.LargestBucket / _buckets.SumTotal) / VERT_RES);
            if (maxY < VERT_RES) maxY = VERT_RES;
            double graphSpan = _buckets.RightEdge - _buckets.LeftEdge;

            // draw the axis
            linePen = new Pen(GRAPH_AXIS_COLOR, _newImageStats.LineWeight);
            g.DrawLine(linePen, graphLeft, graphBottom, graphLeft + graphWidth, graphBottom); // horizontal axis
            g.DrawLine(linePen, graphLeft, graphBottom, graphLeft, graphBottom - graphHeight); // vertical axis

            // draw tick marks
            // WARN: align ticks on 10s, 5s and 2s?
            StringFormat labelFormat = new StringFormat();

            { // draw vertical ticks
                RectangleF labelRect;
                int verticalTicks;

                if (graphHeight < VERT_TICKS * (LABEL_FONT.Height + margin))
                    verticalTicks = (int)Math.Floor((double)graphHeight / (LABEL_FONT.Height + margin));
                else
                    verticalTicks = VERT_TICKS;

                labelFormat.Alignment = StringAlignment.Far;
                float vPoint;
                for (int t = 0; t <= verticalTicks; t++) {
                    vPoint = graphBottom - graphHeight * t / verticalTicks;
                    labelRect = new RectangleF(graphLeft - labelWidth - TICK_LENGTH, vPoint - LABEL_FONT.Height * 0.5f, labelWidth, LABEL_FONT.Height);

                    g.DrawLine(linePen, labelRect.Right, vPoint, graphLeft, vPoint);
                    g.DrawString((maxY * t / verticalTicks).ToString(VERT_LABEL_FORMAT), LABEL_FONT, Brushes.Black, labelRect, labelFormat);
                }
            }

            { // horizontal ticks
                float hPoint;

                // determine maximum horizontal tick label width
                int horizontalPrecision = DEFAULT_HORI_PRECISION;
                string formatString = HORI_LABEL_FORMAT + horizontalPrecision.ToString();
                labelWidth = stringWidth("0.0E+00", LABEL_FONT) * 1.25f;

                // determine number of ticks
                int stepExponent = (int)Math.Ceiling(Math.Log10(graphSpan * labelWidth / graphWidth)) - 1;
                double xMinor = Math.Pow(10, stepExponent);
                double xMajor = xMinor * 10;

                if (labelWidth * graphSpan / xMajor * 5 < graphWidth)
                    xMajor /= 5;
                else if (labelWidth * graphSpan / xMajor * 2 < graphWidth)
                    xMajor /= 2;


                // draw minor ticks
                Pen minorPen = new Pen(GRAPH_AXIS_COLOR, _newImageStats.LineWeight / 2.0f);
                for (double xVal = xMinor * Math.Ceiling(_buckets.LeftEdge / xMinor); xVal <= _buckets.RightEdge; xVal += xMinor) {
                    hPoint = graphLeft + (float)(graphWidth * (xVal - _buckets.LeftEdge) / graphSpan);
                    hPoint = (float)translateRange(xVal, _buckets.LeftEdge, _buckets.RightEdge, graphLeft, graphLeft + graphWidth);
                    g.DrawLine(minorPen, hPoint, graphBottom, hPoint, graphBottom + TICK_LENGTH * MINOR_TICK_SCALE);
                }

                // draw major ticks
                labelFormat.Alignment = StringAlignment.Center;
                float labelOffset = 0.0f - stringWidth("0", LABEL_FONT) / 2;
                if (graphSpan / xMajor < 2) { // only label start and end of graph
                    // left edge label
                    g.DrawLine(linePen, graphLeft, graphBottom, graphLeft, graphBottom + TICK_LENGTH);
                    g.DrawString(_buckets.LeftEdge.ToString(formatString), LABEL_FONT, Brushes.Black, graphLeft + labelOffset, graphBottom + TICK_LENGTH);

                    // right edge label
                    string rightLabel = _buckets.RightEdge.ToString(formatString);
                    hPoint = graphLeft + graphWidth - stringWidth(rightLabel, LABEL_FONT, true);
                    g.DrawLine(linePen, graphLeft + graphWidth, graphBottom, graphLeft + graphWidth, graphBottom + TICK_LENGTH);
                    g.DrawString(rightLabel, LABEL_FONT, Brushes.Black, hPoint, graphBottom + TICK_LENGTH);
                }
                else { // variable number of major labels
                    float thisWidth;
                    for (double xVal = xMajor * Math.Ceiling(_buckets.LeftEdge / xMajor); xVal <= _buckets.RightEdge; xVal += xMajor) {
                        hPoint = (float)translateRange(xVal, _buckets.LeftEdge, _buckets.RightEdge, graphLeft, graphLeft + graphWidth);

                        // draw major tick
                        g.DrawLine(linePen, hPoint, graphBottom, hPoint, graphBottom + TICK_LENGTH);

                        // draw tick label
                        thisWidth = stringWidth(xVal.ToString(formatString), LABEL_FONT, true) / 2;

                        if (hPoint + thisWidth > graphLeft + graphWidth)
                            hPoint = graphLeft + graphWidth - thisWidth;

                        if (hPoint - thisWidth < graphLeft)
                            hPoint = graphLeft + thisWidth + labelOffset;

                        g.DrawString(xVal.ToString(formatString), LABEL_FONT, Brushes.Black, hPoint, graphBottom + TICK_LENGTH, labelFormat);
                    }
                }
            }

            // set up drawing space
            g.Clip = new Region(new Rectangle(graphLeft + 1, graphBottom - graphHeight, graphWidth, graphHeight));
            linePen = new Pen(_newImageStats.OutlineColor, _newImageStats.LineWeight);

            { // draw bar graph
                int thisBucket;
                RectangleF thisBar;
                float barHeight;
                float barWidth = (float)graphWidth / _buckets.BucketCount;
                double barScale = (double)graphHeight / _buckets.SumTotal / maxY;
                for (int i = 0; i < _buckets.BucketCount; i++) {
                    thisBucket = _buckets.Buckets[i];

                    // don't draw empty buckets
                    if (thisBucket > 0) {
                        barHeight = (float)(barScale * thisBucket);
                        thisBar = new RectangleF(barWidth * i + graphLeft, graphBottom - (float)barHeight, barWidth, (float)barHeight);

                        gradientBrush = new LinearGradientBrush(thisBar, _newImageStats.FillGradientStart, _newImageStats.FillGradientEnd, 0.0f);

                        // draw the bar
                        g.FillRectangle(gradientBrush, thisBar);
                        g.DrawRectangle(linePen, thisBar.Left, thisBar.Top, thisBar.Width, thisBar.Height);
                    }
                }
            }

            { // plot each PDF
                PointF[] pdfPoints = new PointF[graphWidth];
                double xStep = graphSpan / graphWidth;
                double xOffset = _buckets.LeftEdge + xStep / 2;
                double yScale = (double)graphHeight / maxY;
                float maxNameLen = 0;
                float thisNameLen;
                Pen pdfPen;

                foreach (KeyValuePair<PDF, Color> thisPDF in _pdfs) {
                    pdfPen = new Pen(thisPDF.Value, _newImageStats.LineWeight * 2);

                    // added
                    if (thisPDF.Key is MultiplePDF) {
                        ((MultiplePDF)thisPDF.Key).transferGreekValues();  // terrible 
                    }

                    for (int b = 0; b < graphWidth; b++) {
                        PointF hax = new PointF((float)(graphLeft + b), (float)((double)graphBottom - thisPDF.Key.getOutput(xOffset + xStep * b) * yScale*BucketList.barWidth2)); // added barwidth2 multiplication to scale pdf to account for problem with matching histogram jim 11/16/12
                        pdfPoints[b] = hax.Y < 0 ? new PointF(hax.X, 0) : hax;
                        object o = hax.Y;
                        Type t = hax.Y.GetType();
                        pdfPoints[b] = hax.Y == Single.NaN ? new PointF(hax.X, 0) : pdfPoints[b];
                        pdfPoints[b] = hax.Y == double.NaN ? new PointF(hax.X, 0) : pdfPoints[b];
                        pdfPoints[b] = hax.Y == float.NaN ? new PointF(hax.X, 0) : pdfPoints[b];
                    }
                    try {
                        g.DrawLines(pdfPen, pdfPoints);
                    }
                    catch (Exception ex) {
                        Program.log(ex.Message + "\r\n" + ex.StackTrace);
                    }
                    

                    thisNameLen = stringWidth(thisPDF.Key.ToString(), LABEL_FONT);
                    if (thisNameLen > maxNameLen)
                        maxNameLen = thisNameLen;
                }

                // draw legend
                float xPoint;
                float yPoint = graphBottom - graphHeight;
                float vSpacing = LABEL_FONT.Height;
                float radius = LABEL_FONT.Height * 0.5f;
                Brush pdfBrush;
                linePen = new Pen(Brushes.Black, LineWeight / 2);
                Brush washout;

                {
                    int argb = GRAPH_BACKGROUND_COLOR.ToArgb();
                    argb &= 0x00ffffff | (LEGEND_OPACITY << 24);
                    washout = new SolidBrush(Color.FromArgb(argb));
                }


                if (maxNameLen > graphWidth)
                    xPoint = graphLeft + margin;
                else
                    xPoint = graphLeft + graphWidth - maxNameLen - radius * 1.5f;

                if (_pdfs.Count > 0)
                    g.FillRectangle(washout, xPoint, yPoint, radius * 1.5f + maxNameLen, vSpacing * _pdfs.Count);

                foreach (KeyValuePair<PDF, Color> thisPDF in _pdfs) {
                    pdfBrush = new SolidBrush(thisPDF.Value);
                    g.FillEllipse(pdfBrush, xPoint, yPoint + (vSpacing - radius) / 2, radius, radius);
                    g.DrawEllipse(linePen, xPoint, yPoint + (vSpacing - radius) / 2, radius, radius);
                    g.DrawString(thisPDF.Key.ToString(), LABEL_FONT, Brushes.Black, xPoint + radius * 1.5f, yPoint);
                    yPoint += vSpacing;
                }
            }

            // synchronize states
            _oldImageStats = _newImageStats.Clone();

            // clean up
            g.Dispose();
        }

        /*
         *  Utility function that measures the width of a string.
         */
        private float stringWidth(string mStr, Font strFont, bool trim = false) {
            Graphics g = Graphics.FromImage(new Bitmap(_newImageStats.ImageSize.Width, strFont.Height));
            g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
            float imgWidth;

            if (trim) {
                imgWidth = (float)g.MeasureString(mStr, strFont, new PointF(0.0f, 0.0f), StringFormat.GenericTypographic).Width;
            }
            else
                imgWidth = (float)g.MeasureString(mStr, strFont).Width;

            g.Dispose();
            return imgWidth;
        }

        /*
         *  Utility function to translate a value in one range to another
         */
        public double translateRange(double val, double srcStart, double srcEnd, double dstStart, double dstEnd) {
            return dstStart + (val - srcStart) * (dstEnd - dstStart) / (srcEnd - srcStart);
        }

        /*
         *	The following methods provide compatibility with previous version
         *	of the Histogram class.
         */

        public Bitmap GetHistogramWithBucketCount(Size size, int barCount, PDF pdf = null, bool isMixed = false, int lineWeight = DEFAULT_LINE_WEIGHT) {
            // push relevant histogram info
            Dictionary<PDF, Color> holdPDFs = _pdfs;
            HistogramDescriptor holdDesc = _newImageStats;
            BucketsDescriptor holdBucket = _bucketStats;

            // set temporary histogram info
            _bucketStats = new BucketsDescriptor(barCount, holdBucket.Start, holdBucket.End);
            _bucketStats.BucketCount = barCount;

            _newImageStats = holdDesc.Clone();
            _newImageStats.ImageSize = size;
            _newImageStats.LineWeight = lineWeight;

            _pdfs = new Dictionary<PDF, Color>();
            if (pdf != null)
                this.AddPDF(pdf, DEFAULT_PDF_COLOR);

            // draw it
            Bitmap hist = this.GetHistogram();

            // pop original histogram info
            _pdfs = holdPDFs;
            _newImageStats = holdDesc;
            _bucketStats = holdBucket;

            return hist;
        }

        public Bitmap GetHistogramBitmapWithColor(Size size, Color color, PDF pdf = null, bool isMixed = false, int lineWeight = DEFAULT_LINE_WEIGHT) {
            // push relevant histogram info
            Dictionary<PDF, Color> holdPDFs = _pdfs;
            HistogramDescriptor holdDesc = _newImageStats;

            // set temporary histogram info
            _newImageStats = holdDesc.Clone();
            _newImageStats.ImageSize = size;
            _newImageStats.FillGradientStart = color;
            _newImageStats.FillGradientEnd = color;
            _newImageStats.LineWeight = lineWeight;

            _pdfs = new Dictionary<PDF, Color>();
            if (pdf != null)
                this.AddPDF(pdf, DEFAULT_PDF_COLOR);

            // draw it
            Bitmap hist = this.GetHistogram();

            // pop original histogram info
            _pdfs = holdPDFs;
            _newImageStats = holdDesc;

            return hist;
        }

        public Bitmap GetHistogramBitmap(Size size, string title = "", PDF pdf = null, bool isMixed = false, int lineWeight = DEFAULT_LINE_WEIGHT) {
            return GetHistogramBitmap(size, pdf, lineWeight);
        }

        public Bitmap GetHistogramBitmap(Size size, PDF pdf, int lineWeight = DEFAULT_LINE_WEIGHT) {
            // push relevant histogram info
            Dictionary<PDF, Color> holdPDFs = _pdfs;
            HistogramDescriptor holdDesc = _newImageStats;

            // set temporary histogram info
            _newImageStats = holdDesc.Clone();
            _newImageStats.ImageSize = size;
            _newImageStats.LineWeight = lineWeight;

            _pdfs = new Dictionary<PDF, Color>();
            if (pdf != null)
                this.AddPDF(pdf, DEFAULT_PDF_COLOR);

            // draw it
            Bitmap hist = this.GetHistogram();

            // pop original histogram info
            _pdfs = holdPDFs;
            _newImageStats = holdDesc;

            return hist;
        }
    }
}
